package org.xx.armory.commons;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Base64;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.xx.armory.commons.Validators.greaterThanOrEqual;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notNull;

/**
 * 签名和验证签名的工具类。
 * <p>必须通过{@link #getInstance()}获得一个实例</p>
 */
@Immutable
public final class SignHelper {
    private static final String DEFAULT_SIGN_ALGORITHM = "SHA1withRSA";
    private static final int DEFAULT_MAX_DIGEST_BLOCK_SIZE = 512;

    private final Logger logger = LoggerFactory.getLogger(SignHelper.class);

    private final Signature signature;
    private final int maxDigestBlockSize;

    /**
     * 构造一个签名和验证签名的对象。
     *
     * @param algorithm
     *         签名或者验证签名的算法。
     * @param maxDigestBlockSize
     *         执行算法时，每个摘要块的最大大小，该值不能小于{@literal 16}。
     * @throws IllegalArgumentException
     *         如果参数{@code algorithm}是{@code null}或者只包含空白字符；或者参数{@code maxDigestBlockSize}小于{@literal 16}。
     * @throws NoSuchAlgorithmException
     *         如果当前JVM找不到指定的算法。
     */
    private SignHelper(
            String algorithm,
            int maxDigestBlockSize
    )
            throws NoSuchAlgorithmException {
        algorithm = notBlank(algorithm, "algorithm").trim().toUpperCase();
        this.signature = Signature.getInstance(algorithm);
        this.maxDigestBlockSize = greaterThanOrEqual(maxDigestBlockSize, "maxDigestBlockSize", DEFAULT_MAX_DIGEST_BLOCK_SIZE);
    }

    /**
     * 构造一个签名和验证签名的对象，使用默认的算法和默认的摘要块大小。
     *
     * @return 已构造的签名和验证签名对象。
     */
    public static SignHelper getInstance() {
        try {
            return getInstance(DEFAULT_SIGN_ALGORITHM, DEFAULT_MAX_DIGEST_BLOCK_SIZE);
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException("cannot find algorithm: " + DEFAULT_SIGN_ALGORITHM, ex);
        }
    }

    /**
     * 构造一个签名和验证签名的对象，使用默认的摘要块大小。
     *
     * @param algorithm
     *         签名或者验证签名的算法。
     * @return 已构造的签名和验证签名对象。
     * @throws IllegalArgumentException
     *         如果参数{@code algorithm}是{@code null}或者只包含空白字符。
     * @throws NoSuchAlgorithmException
     *         如果当前JVM找不到指定的算法。
     */
    public static SignHelper getInstance(
            String algorithm
    )
            throws NoSuchAlgorithmException {
        return getInstance(algorithm, 16);
    }

    /**
     * 构造一个签名和验证签名的对象。
     *
     * @param algorithm
     *         签名或者验证签名的算法。
     * @param maxDigestBlockSize
     *         执行算法时，每个摘要块的最大大小，该值不能小于{@literal 16}。
     * @return 已构造的签名和验证签名对象。
     * @throws IllegalArgumentException
     *         如果参数{@code algorithm}是{@code null}或者只包含空白字符；或者参数{@code maxDigestBlockSize}小于{@literal 16}。
     * @throws NoSuchAlgorithmException
     *         如果当前JVM找不到指定的算法。
     */
    public static SignHelper getInstance(
            String algorithm,
            int maxDigestBlockSize
    )
            throws NoSuchAlgorithmException {
        return new SignHelper(algorithm, maxDigestBlockSize);
    }

    /**
     * 验证签名
     *
     * @param content
     *         待验证的文本。
     * @param signature
     *         HEX编码的签名。
     * @param publicKey
     *         用于验证签名的公钥。
     * @return 如果需要签名的文本与签名结果匹配则返回 {@code true}, 否则返回{@code false}, 如果需要验证签名的文本是{@code null}或者空字符串则不执行验证，直接返回{@code true}。
     * @throws IllegalArgumentException
     *         如果公钥是{@code null}。
     * @throws InvalidKeyException
     *         如果参数{@code publicKey}不可用。
     * @throws SignatureException
     *         如果执行验证签名时出现错误。
     * @see #verify(String, Charset, byte[], PublicKey)
     */
    public boolean verifyHex(
            String content,
            String signature,
            PublicKey publicKey
    )
            throws InvalidKeyException, SignatureException {
        notNull(publicKey, "publicKey");

        logger.debug("verify: \"{}\"\n with signature(hex): \"{}\"", content, signature);

        return verify(content, UTF_8, Converter.toBytes(signature), publicKey);
    }

    /**
     * 验证签名
     *
     * @param content
     *         待验证的文本。
     * @param signature
     *         Base64编码的签名结果
     * @param publicKey
     *         用于验证签名的公钥。
     * @return 如果需要签名的文本与签名结果匹配则返回 {@code true}, 否则返回{@code false}, 如果需要验证签名的文本是{@code null}或者空字符串则不执行验证，直接返回{@code true}。
     * @throws IllegalArgumentException
     *         如果公钥是{@code null}。
     * @throws InvalidKeyException
     *         如果参数{@code publicKey}不可用。
     * @throws SignatureException
     *         如果执行验证签名时出现错误。
     * @see #verify(String, Charset, byte[], PublicKey)
     */
    public boolean verifyBase64(
            String content,
            String signature,
            PublicKey publicKey
    )
            throws InvalidKeyException, SignatureException {
        notNull(publicKey, "publicKey");

        logger.debug("verify: \"{}\"\n with signature(base64): \"{}\"", content, signature);

        return verify(content, UTF_8, Base64.getDecoder().decode(signature), publicKey);
    }

    /**
     * 验证签名
     *
     * @param content
     *         待验证的文本。
     * @param charset
     *         用于将文本编码为字节数组的字符集。
     * @param signature
     *         签名。
     * @param publicKey
     *         用于验证签名的公钥。
     * @return 如果需要签名的文本与签名结果匹配则返回 {@code true}, 否则返回{@code false}, 如果需要验证签名的文本是{@code null}或者空字符串则不执行验证，直接返回{@code true}。
     * @throws IllegalArgumentException
     *         如果公钥是{@code null}。
     * @throws InvalidKeyException
     *         如果参数{@code publicKey}不可用。
     * @throws SignatureException
     *         如果执行验证签名时出现错误。
     */
    public boolean verify(
            String content,
            Charset charset,
            byte[] signature,
            PublicKey publicKey
    )
            throws InvalidKeyException, SignatureException {
        notNull(charset, "charset");
        notNull(publicKey, "publicKey");

        if (isEmpty(content)) {
            return true;
        }

        this.signature.initVerify(publicKey);

        int p = 0;
        final int l = content.length();
        final int lr = l - this.maxDigestBlockSize;
        while (p < lr) {
            final int pe = p + this.maxDigestBlockSize;
            this.signature.update(content.substring(p, pe).getBytes(charset));
            p = pe;
        }
        if (p < l) {
            this.signature.update(content.substring(p).getBytes(charset));
        }

        return this.signature.verify(signature);
    }

    /**
     * 获取Base64编码的签名结果
     *
     * <p>文本会被按照UTF_8编码，然后签名；签名后的结果会按照Base64编码为字符串。</p>
     *
     * @param content
     *         待签名的文本。
     * @param privateKey
     *         用于签名的私钥。
     * @return Base64编码的签名结果, 如果需要签名的文本是{@code null}或者空字符串, 那么返回空字符串。
     * @throws IllegalArgumentException
     *         如果私钥是{@code null}。
     * @throws InvalidKeyException
     *         如果参数{@code privateKey}不可用。
     * @throws SignatureException
     *         如果执行签名时出现错误。
     * @see #sign(String, Charset, PrivateKey)
     */
    public String signBase64(
            String content,
            PrivateKey privateKey
    )
            throws InvalidKeyException, SignatureException {
        notNull(privateKey, "privateKey");

        logger.debug("sign: \"{}\"\n with private key", content);

        return Base64.getEncoder().encodeToString(sign(content, UTF_8, privateKey));
    }

    /**
     * 签名
     *
     * @param content
     *         待签名的文本。
     * @param charset
     *         用于将文本编码为字节数组的字符集。
     * @param privateKey
     *         用于签名的私钥。
     * @return 签名结果。
     * @throws InvalidKeyException
     *         如果参数{@code privateKey}不可用。
     * @throws SignatureException
     *         如果执行签名时出现错误。
     */
    public byte[] sign(
            String content,
            Charset charset,
            PrivateKey privateKey
    )
            throws InvalidKeyException, SignatureException {
        notNull(charset, "charset");
        notNull(privateKey, "privateKey");

        if (isEmpty(content)) {
            return new byte[0];
        }

        this.signature.initSign(privateKey);

        int p = 0;
        final int l = content.length();
        final int lr = l - this.maxDigestBlockSize;
        while (p < lr) {
            final int pe = p + this.maxDigestBlockSize;
            this.signature.update(content.substring(p, pe).getBytes(charset));
            p = pe;
        }
        if (p < l) {
            this.signature.update(content.substring(p).getBytes(charset));
        }

        return this.signature.sign();
    }
}
